Mastering Hero Animations in Flutter

Chema Molins
Flutter Community
Published in
6 min readNov 12, 2018

--

You may be asking, what should I know about Hero animations that I don’t already know?

That may be true.

In fact, Flutter makes displaying Hero animations really trivial. Wrap the widgets that you want to animate to and from with the Hero widget, give them the same tag and you’re done.

But, what if you want to achieve the following?

After this, a normal Hero animation looks like a little boring! Doesn’t it?

Mastering Hero Animations has two sides:

  • Knowing all the possibilities that the widget offers to make astonishing UIs.
  • Understanding what is happening behind the scenes; how is the flight of the widget triggered by the NavigatorObserver? how does the flight takes place on the Navigator’s Overlay? and so on.

The first one will be enough if you just try to squeeze the Hero possibilities to apply to your app.

But if you want to go beyond and understand how all this is orchestrated you will need to go deeper into the Flutter framework.

My intention is to cover both aspects, but since the post was getting very long, I have decided to leave the architecture and internals explanation for a second part.

The topics discussed in this post will lay the ground for what will come in the next one.

(For the tests I’m using the application built in Madrid Flutter Study Jam).

Let’s structure the post around Hero properties or parameters.

Hero Properties

We will better understand Hero properties if we look at it in slow motion (use timeDilation for that). You literally see the widget “flying” from one page to the other.

So, how can we customise the behaviour of the Hero animation? This is the definition of the Hero widget:

class Hero extends StatefulWidget {

final Object tag;
final HeroFlightShuttleBuilder flightShuttleBuilder;
final CreateRectTween createRectTween;
final TransitionBuilder placeholderBuilder;
final Widget child;
...
}

Let’s see these properties one at a time.

final HeroFlightShuttleBuilder flightShuttleBuilder

flightShuttleBuilder is a builder that lets you change what you see on the overlay during the flight. By default, it returns the hero widget itself but you can return whatever widget you want.

The builder takes several parameters, one of them being the animation itself. By manipulating the animation, you can do things like the following:

And this is the code:

flightShuttleBuilder: (
BuildContext flightContext,
Animation<double> animation,
HeroFlightDirection flightDirection,
BuildContext fromHeroContext,
BuildContext toHeroContext,
) {
final Hero toHero = toHeroContext.widget;
return RotationTransition(
turns: animation,
child: toHero.child,
);
}

The transition is easy because we use the animation as it is given in the parameter list. The animation provides a liner interpolation of double values and is used as such in the RotationTransition.

But we can tweak/control the animation to have not so boring transitions. In the following code we are fading the hero by providing a quadratic function to the animation.

flightShuttleBuilder: (
....
) {
final Hero toHero = toHeroContext.widget;
return FadeTransition(
opacity: animation.drive(
Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(
curve: Interval(0.0, 1.0,
curve: ValleyQuadraticCurve()),
),
),
),
child: toHero.child,
);
}
class ValleyQuadraticCurve extends Curve {
@override
double transform(double t) {
assert(t >= 0.0 && t <= 1.0);
return 4 * math.pow(t - 0.5, 2);
}
...

Nice!

Or applied to a ScaleTransition

In this case we use a different quadratic function where we have a peak instead of a valley.

flightShuttleBuilder: (
....
) {
final Hero toHero = toHeroContext.widget;
return ScaleTransition(
scale: animation.drive(
Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(
curve: Interval(0.0, 1.0,
curve: PeakQuadraticCurve()),
),
),
),
child: toHero.child,
);
}
class PeakQuadraticCurve extends Curve {
@override
double transform(double t) {
assert(t >= 0.0 && t <= 1.0);
return -15 * math.pow(t, 2) + 15 * t + 1;
}
...

Finally, we can combine all of these transitions to produce the first animation in the post. Rotating hero when pushing and fading hero when popping.

flightShuttleBuilder: (
....
) {
final Hero toHero = toHeroContext.widget;
return ScaleTransition(
scale: animation.drive(
Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(
curve: Interval(0.0, 1.0,
curve: PeakQuadraticCurve()),
),
),
),
child: flightDirection == HeroFlightDirection.push
? RotationTransition(
turns: animation,
child: toHero.child,
)
: FadeTransition(
opacity: animation.drive(
Tween<double>(begin: 0.0, end: 1.0).chain(
CurveTween(
curve: Interval(0.0, 1.0,
curve: ValleyQuadraticCurve()),
),
),
),
child: toHero.child,
),
);
}

final CreateRectTween createRectTween

As the definition in the doc says “Defines how the destination hero’s bounds change as it flies from the starting route to the destination route.”

CreateRectTween is a function that returns a RectTween and controls the alignment and position of the rectangle where the flying widget is placed during the flight. By default, the path is defined by the function MaterialRectArcTween and the size of the rectangle is a linear interpolation between the from and to rectangles.

Normally, we would not need to change the default behaviour. The official docs change it for the radial hero animation demo.

But, what if you want to deviate from the Material guidelines and standard arc path and do something like the following?

This might seem awkward in some transitions, but the possibility is there.

You need to create your custom CreateRectTween function returning a customRectTween.

static RectTween _createRectTween(Rect begin, Rect end) {
return QuadraticRectTween(begin: begin, end: end);
}
...
Hero(
tag: "thisistheherotag",
createRectTween: _createRectTween,
...

QuadraticRectTween is just a copy of MaterialRectCenterArcTween where I have replaced the call toMaterialPointArcTween in the _initialize() method by the following class:

class QuadraticOffsetTween extends Tween<Offset> {

QuadraticOffsetTween({
Offset begin,
Offset end,
}) : super(begin: begin, end: end);


@override
Offset lerp(double t) {
if (t == 0.0)
return begin;
if (t == 1.0)
return end;
final double x = -11 * begin.dx * math.pow(t, 2) +
(end.dx + 10 * begin.dx) * t + begin.dx;
final double y = -2 * begin.dy * math.pow(t, 2) +
(end.dy + 1 * begin.dy) * t + begin.dy;
return Offset(x, y);
}
}

It provides a path based on the quadratic functions for x and y center properties.

Using the CreateRectTween you could also change the size of the rectangle, though this is something we have already achieved previously by using ScaleTransition.

final TransitionBuilder placeholderBuilder

The last parameter to analyse is placeholderBuilder.

As we will see in the next post, the Hero is very basic. Its build() method either renders the child widget when no flight is active (as any other parent widget), or the placeholder widget when the flight is active. If no placeholderBuilder is provided, it just renders en empty SizedBox.

In the following image you can see the child widget itself faded in its original place.

Passing a placeholderBuilder not always makes sense, as in this example, but it can provide a hint to the user that something is missing in that place when the Hero is flying.

Hero(
tag: "thisistheherotag",
placeholderBuilder: (context, child) {
return Opacity(opacity: 0.2, child: child);
},

The builder receives the child as a parameter, but nothing prevents you of returning any other widget.

And that’s all. I hope you found the flight interesting. In the next post I will get into the internals of the Hero flight itself.

You can follow me in my twitter profile for updates.

--

--